package com.hailiang.saas.auth.filter;

import com.alibaba.fastjson.JSON;
import com.hailiang.mp.commonsource.api.CommonResult;
import com.hailiang.saas.auth.common.Constant;
import com.hailiang.saas.auth.common.DefaultUserDetails;
import com.hailiang.saas.auth.config.AuthenticationConfig;
import com.hailiang.saas.auth.service.CommonService;
import com.hailiang.saas.auth.service.JwtTokenService;
import com.hailiang.saas.auth.strategy.AuthFilterStrategy;
import com.hailiang.saas.auth.emuns.AuthChannelEnum;
import com.hailiang.saas.auth.emuns.AuthEndpointEnum;
import com.hailiang.saas.auth.vo.AuthenticationResource;
import com.hailiang.saas.auth.vo.LoginAuthProtocol;
import com.hailiang.saas.auth.vo.login.PasswordLoginProtocol;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.jwk.RSAKey;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.Date;
import java.util.HashSet;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * 同一认证过滤器  业务URL: /sso/channel/login/auth
 * @author : pangfuzhong
 * @description
 * @date : 2021/9/27 10:06
 */

@Slf4j
public class PasswordAuthFilter extends UsernamePasswordAuthenticationFilter implements AuthFilterStrategy {
    private static AuthenticationConfig authenticationConfig;
    private static RedisTemplate redisTemplate;

    public PasswordAuthFilter(AuthenticationConfig config) {
        authenticationConfig = config;
    }

    @Override
    public AuthEndpointEnum getAuthEndpointEnum() {
        return AuthEndpointEnum.WEB_BROWSER;
    }

    @Override
    public AuthChannelEnum getAuthChannelEnum() {
        return AuthChannelEnum.PASSWORD;
    }

    @Override
    public String filterUrl() {
        return authenticationConfig.getLoginAuthUrl();
    }

    @Override
    public Boolean match(HttpServletRequest request, HttpServletResponse response) {
        if(!StringUtils.contains(request.getRequestURL().toString(), this.filterUrl())) {
            return Boolean.FALSE;
        }

        // 登陆方式(PC MOBILE)
        String endPoint = request.getHeader(Constant.ENDPOINT_HEADER);
        if(StringUtils.isBlank(endPoint)) {
            return Boolean.FALSE;
        }

        // 登陆渠道(PASSWORD VERIFICATION)
        String channel = request.getHeader(Constant.CHANNEL_HEADER);
        if(StringUtils.isBlank(endPoint)) {
            return Boolean.FALSE;
        }

        // 登陆方式合法性
        AuthEndpointEnum authEndpointEnum = AuthEndpointEnum.getByCode(endPoint);
        if(Objects.isNull(authEndpointEnum)) {
            return Boolean.FALSE;
        }

        // 登陆渠道合法性
        AuthChannelEnum authChannelEnum = AuthChannelEnum.getByCode(channel);
        if(Objects.isNull(authChannelEnum)) {
            return Boolean.FALSE;
        }

        // 是否匹配当前过滤器处理
        if(!(authEndpointEnum.equals(this.getAuthEndpointEnum()) && authChannelEnum.equals(this.getAuthChannelEnum()))) {
            return Boolean.FALSE;
        }

        return Boolean.TRUE;
    }

    /**
     * 如果是登陆则校验此次登陆认证的方式, 如果endpoint是PC_WEB 及 channel是PASSWORD 方式则执行此过滤器
     * @param request
     * @param response
     * */
    @Override
    protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
        return this.match(request, response);
    }

    /**
     * 尝试认证
     * @param request
     * @param response
     * */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (!request.getMethod().equals(Constant.POST)) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
            redisTemplate = authenticationConfig.getRedisTemplate();
            String endPoint = request.getHeader(Constant.ENDPOINT_HEADER);
            String channel = request.getHeader(Constant.CHANNEL_HEADER);
            String username = this.obtainUsername(request);
            String password = this.obtainPassword(request);
            if (username == null) {
                username = "";
            }

            if (password == null) {
                password = "";
            }
            username = username.trim();

            // 密码登陆认证协议
            PasswordLoginProtocol passwordLoginProtocol = new PasswordLoginProtocol();
            passwordLoginProtocol.setEndPoint(endPoint);
            passwordLoginProtocol.setChannel(channel);
            passwordLoginProtocol.setAccountName(username);
            passwordLoginProtocol.setPassword(password);
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(passwordLoginProtocol.getAccountName(), passwordLoginProtocol.getPassword());

            // 1、校验当前账号是否在该登陆方式及渠道重新登陆
            this.checkRepeatAuth(authRequest, passwordLoginProtocol);

            // 2、登陆认证
            authRequest.setDetails(passwordLoginProtocol);
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }

    /**
     * 校验当前账号是否在该渠道重复登录
     * @param authRequest
     * */
    private void checkRepeatAuth(UsernamePasswordAuthenticationToken authRequest, PasswordLoginProtocol passwordLoginProtocol) {
        String endpointChannelKey = MessageFormat.format("{0}_{1}_{2}", authRequest.getPrincipal().toString(), passwordLoginProtocol.getEndPoint(), passwordLoginProtocol.getChannel());
        String offlineKey = "";
        try {
            // 1、判断是否登陆过(key: 账号_登陆方式_登陆渠道)
            Object tokenValue = redisTemplate.opsForValue().get(endpointChannelKey);
            if(Objects.isNull(tokenValue)) {
                // 不存在，说明当前账号的登陆方式及渠道没有重新发生过登陆，那直接返回登陆认证
                return;
            }

            // 2、存在, 设置当前token值为被踢出的token, 执行到此处表示已经发生了web浏览器重复登录过程
            offlineKey = MessageFormat.format("{0}_{1}_{2}", Constant.OFFLINE_KEY, Constant.TOKEN_KEY_PREFIX, tokenValue.toString());
            redisTemplate.opsForValue().set(offlineKey, Constant.OFFLINE_VALUE, 1, TimeUnit.DAYS);
            // 如果删除失败也无所谓, 后面重新登陆成功后依旧会设置该key-value
            redisTemplate.delete(endpointChannelKey);
        } catch (Exception e) {
            log.debug("--> PasswordAuthFilter: 校验账号是否重复登陆出现异常, error = {}", e.getMessage());
            if(!StringUtils.isBlank(offlineKey) && redisTemplate.hasKey(offlineKey)) {
                return;
            }

            throw new UsernameNotFoundException("检测到重复登陆, 设置ELSE_WHERE_token重复登录状态异常" + e.getMessage() + "");
        }
    }

    /**
     * 设置认证成功后回调
     * @param authenticationSuccessHandler
     * */
    @Override
    public void setAuthenticationSuccessHandler(AuthenticationSuccessHandler authenticationSuccessHandler) {
        if(!Objects.isNull(authenticationSuccessHandler)) {
            super.setAuthenticationSuccessHandler(authenticationSuccessHandler);
            return;
        }
        super.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {

            @Override
            public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                // 1、认证成功，生成token
                PasswordLoginProtocol passwordLoginProtocol = (PasswordLoginProtocol) authentication.getDetails();
                DefaultUserDetails userDetails = (DefaultUserDetails) authentication.getPrincipal();

                // 2、登陆认证返回
                LoginAuthProtocol loginAuthProtocol = new LoginAuthProtocol();
                loginAuthProtocol.setEndPoint(passwordLoginProtocol.getEndPoint());
                loginAuthProtocol.setChannel(passwordLoginProtocol.getChannel());
                AuthenticationResource authenticationResource = new AuthenticationResource();
                authenticationResource.setUserName(userDetails.getUsername());
                authenticationResource.setAccountSource(userDetails.getAccountSource());
                authenticationResource.setUcUserInfoVO(userDetails.getUcUserInfoVO());
                authenticationResource.setRoles(new HashSet<>());
                authenticationResource.setPermissions(new HashSet<>());
                loginAuthProtocol.setLoginDate(new Date());
                loginAuthProtocol.setAuthenticationResource(authenticationResource);
                userDetails.setTimestamp(loginAuthProtocol.getLoginDate().getTime());
                userDetails.setPassword("");
                log.info("--> PcPasswordAuthFilter: 用户登陆认证成功, 认证信息: endPoint = {}, channel = {}, userName = {}", userDetails.getUsername(), loginAuthProtocol.getEndPoint(), loginAuthProtocol.getChannel());

                // 3、生成token(JwtToken、uuid、分布式id)
                String token = this.generateJwtToken(userDetails);
                String endpointChannelKey = MessageFormat.format("{0}_{1}_{2}", userDetails.getUsername(), loginAuthProtocol.getEndPoint(), loginAuthProtocol.getChannel());
                String tokenKey = MessageFormat.format("{0}_{1}", Constant.TOKEN_KEY_PREFIX, token);
                //String token = UUID.randomUUID().toString();

                // 3、token存入redis
                CommonResult commonResult = new CommonResult();commonResult.setSuccess(Boolean.TRUE);
                if(StringUtils.isBlank(token)) {
                    commonResult.setSuccess(Boolean.FALSE);
                    commonResult.setErrorCode(Constant.TOKEN_GENERATE_FAILURE_CODE);
                    commonResult.setMsg(Constant.TOKEN_GENERATE_FAILURE_MSG);
                    log.info("--> PcPasswordAuthFilter: 用户登陆认证失败, 生成token异常: endPoint = {}, channel = {}, userName = {}", userDetails.getUsername(), loginAuthProtocol.getEndPoint(), loginAuthProtocol.getChannel());
                } else {
                    authenticationResource.setAccessToken(token);
                    RedisTemplate redisTemplate = authenticationConfig.getRedisTemplate();
                    try {
                        /**
                         * 存储两份key-value, 此时两份key必须同时存储redis成功
                         *  1、记录账号登陆端情况, 表示此账号以什么方式登陆, 做互踢使用
                         *       key: 账号 + 登陆方式 + 登陆渠道
                         *       value: token
                         *  2、记录账号token与用户信息, 用于认证使用
                         *      key: token_****
                         *      value: 账号及用户信息字符串
                         * */
                        redisTemplate.opsForValue().set(endpointChannelKey, token);
                        TimeUnit timeUnit = StringUtils.isBlank(authenticationConfig.getTokenExpireTimeUnit())?TimeUnit.HOURS:TimeUnit.valueOf(authenticationConfig.getTokenExpireTimeUnit());
                        redisTemplate.opsForValue().set(tokenKey, JSON.toJSONString(loginAuthProtocol), authenticationConfig.getTokenExpireTime(), timeUnit);
                        commonResult.setObject(loginAuthProtocol);
                        log.info("--> PcPasswordAuthFilter: 用户登陆认证成功, 凭据token生成成功: userName = {}, token = {}, endPoint = {}, channel = {}", userDetails.getUsername(), tokenKey, loginAuthProtocol.getEndPoint(), loginAuthProtocol.getChannel());
                    } catch (Exception e) {
                        this.exceptionHandler(endpointChannelKey, tokenKey, redisTemplate);
                        commonResult.setSuccess(Boolean.FALSE);
                        commonResult.setErrorCode(Constant.TOKEN_STORE_REDIS_FAILURE_CODE);
                        commonResult.setMsg(Constant.TOKEN_STORE_REDIS_FAILURE_MSG + ":" + e.getMessage());
                        log.info("--> PcPasswordAuthFilter: 用户登陆认证失败, 存储token过程产生异常: endPoint = {}, channel = {}, userName = {}", userDetails.getUsername(), loginAuthProtocol.getEndPoint(), loginAuthProtocol.getChannel());
                    }
                }

                // 4、返回
                CommonService.returnResponse(response, commonResult);
            }

            private void exceptionHandler(String endpointChannelKey, String tokenKey, RedisTemplate redisTemplate) {
                if(redisTemplate.hasKey(endpointChannelKey)) {
                    redisTemplate.delete(endpointChannelKey);
                }

                if(redisTemplate.hasKey(tokenKey)) {
                    redisTemplate.delete(tokenKey);
                }

                // todo 异常依旧没有删除成功
            }

            private String generateJwtToken(DefaultUserDetails userDetails) {
                String jwtToken = "";
                try {
                    JwtTokenService jwtTokenService = authenticationConfig.getJwtTokenService();
                    RSAKey rsaKey = JwtTokenService.rsaKeyMap.get("RSAKEY");
                    if(Objects.isNull(rsaKey)) {
                        // 生成RsKey
                        rsaKey = jwtTokenService.generateDefaultRSAKey();
                    }

                    // 利用RsKey对应用户信息加密后生成jwtToken
                    jwtToken = jwtTokenService.generateTokenByRSA(JSON.toJSONString(userDetails), rsaKey);
                } catch (JOSEException e) {
                    e.printStackTrace();
                }

                return jwtToken;
            }
        });
    }

    /**
     * 设置认证失败后回调
     * @param authenticationFailureHandler
     * */
    @Override
    public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
        if(!Objects.isNull(authenticationFailureHandler)) {
            super.setAuthenticationFailureHandler(authenticationFailureHandler);
            return;
        }
        super.setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
            @Override
            public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException ae) throws IOException, ServletException {
                // 1、认证失败, 用户名密码不存在
                CommonResult commonResult = new CommonResult();
                commonResult.setSuccess(Boolean.FALSE);
                commonResult.setErrorCode(Constant.LOGIN_AUTH_FAILURE_CODE);
                commonResult.setMsg(Constant.LOGIN_AUTH_FAILURE_MSG + " : " + (ae.getMessage().contains("Bad credentials")? "密码不正确":ae.getMessage()));
                CommonService.returnResponse(response, commonResult);
            }
        });
    }

    @Override
    public void setUsernameParameter(String parameter) {
        super.setUsernameParameter(parameter);
    }

    @Override
    public void setPasswordParameter(String parameter) {
        super.setPasswordParameter(parameter);
    }
}
